#include <QMouseEvent>
#include "mesh_widget.h"
#include <cfloat>
#include <cmath>
#include <cassert>

namespace widgets {
  using namespace boost;

  Mesh::Mesh(QWidget *parent):
  QGLWidget(parent),
    pan_(0, 0),
    rotation_(45.0, 45.0),
    zoom_(1.0),
    wireframe_(false),
    selectedItemType(IMesh::element_triangle),
    selectedItemIndex(0),
    m_Stops(3)
  {
    m_Stops << QGradientStop(0.0, Qt::black) << QGradientStop(0.5, Qt::red) << QGradientStop(1.0, Qt::white);
    m_StopMinValue = 0.0;
    m_StopMaxValue = 1.0;

    memset(m_MatrixRotation, 0, sizeof(m_MatrixRotation));
    m_MatrixRotation[0] = m_MatrixRotation[5] = m_MatrixRotation[10] = m_MatrixRotation[15] = 1.0;

    memset(m_MatrixRotationManipulator, 0, sizeof(m_MatrixRotationManipulator));
    m_MatrixRotationManipulator[0] = m_MatrixRotationManipulator[5] = m_MatrixRotationManipulator[10] = m_MatrixRotationManipulator[15] = 1.0;
  }

  Mesh::~Mesh()
  {
    updateGL();
  }

  void Mesh::AddMesh(const QString & Name_, IMesh *pMesh_)
  {
    mMeshContainer[Name_] = pMesh_;
    updateGL();
  }

  void Mesh::RecalculateBounds()
  {
    bounds[0].x = bounds[0].y = bounds[0].z = FLT_MAX;
    bounds[1].x = bounds[1].y = bounds[1].z = FLT_MIN;

    foreach (IMesh *pMesh, mMeshContainer)
    {
      const IMesh::t_container_vertex &vertices = pMesh->get_vertices();
      const size_t vertexQty = vertices.size();
      
      for (int i = 0; i < vertexQty; i++)
      {
        bounds[0].x = qMin(bounds[0].x, vertices[i].x);
        bounds[1].x = qMax(bounds[1].x, vertices[i].x);

        bounds[0].y = qMin(bounds[0].y, vertices[i].y);
        bounds[1].y = qMax(bounds[1].y, vertices[i].y);

        bounds[0].z = qMin(bounds[0].z, vertices[i].z);
        bounds[1].z = qMax(bounds[1].z, vertices[i].z);
      }
    }
  }

  bool Mesh::get_wireframe()
  {
    return wireframe_;
  }

  const QGradientStops & Mesh::GetStops() const
  {
    return m_Stops;
  }

  void Mesh::set_pan(qreal x_pan, qreal y_pan)
  {
    pan_.setX(x_pan);
    pan_.setY(y_pan);
    updateGL();
  }

  void Mesh::set_rotation(qreal z_axis, qreal x_axis)
  {
//    glPushMatrix();
    glLoadIdentity();
    glRotatef(z_axis, 0.0, 1.0, 0.0);
    glRotatef(x_axis, 1.0, 0.0, 0.0);
    glGetDoublev(GL_MODELVIEW_MATRIX,  m_MatrixRotationManipulator);
//    glPopMatrix();

    updateGL();
  }

  void Mesh::set_zoom(qreal zoom)
  {
    if (zoom_ != zoom)
    {
      zoom_ = zoom;
      updateGL();
    }
  }

  void Mesh::set_wireframe(bool wireframe)
  {
    if (wireframe_ != wireframe)
    {
      wireframe_ = wireframe;
      updateGL();
    }
  }

  void Mesh::toggle_wireframe()
  {
    set_wireframe(!get_wireframe());
  }

  void Mesh::SetStops(const QGradientStops & _Stops, qreal _MinValue, qreal _MaxValue, const QString & _Caption)
  {
    m_Stops        = _Stops;
    m_StopMinValue = _MinValue;
    m_StopMaxValue = _MaxValue;
    m_Caption      = _Caption;

    updateGL();
  }

  void Mesh::initializeGL()
  {
    qglClearColor(Qt::white);
    glShadeModel(GL_SMOOTH);
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);
  }

  void Mesh::paintGL()
  {
    GLint viewport[4];
    glGetIntegerv(GL_VIEWPORT, viewport);
    double k = (viewport[3] > 0) ? double(viewport[2]) / viewport[3] : 1.0;
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-zoom_ / 2 * k, zoom_ / 2 * k, -zoom_ / 2, zoom_ / 2, -1000, 1000);

    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslated(pan_.x(), pan_.y(), 0.0);

    glMultMatrixd(m_MatrixRotationManipulator);
    glMultMatrixd(m_MatrixRotation);
    
    { // bounding scale
      const GLdouble boundMax = qMax(
          qMax(
              bounds[1].x - bounds[0].x,
              bounds[1].y - bounds[0].y
            ),
          bounds[1].z - bounds[0].z
        );
      const GLdouble boundsScale = 1.0 / (boundMax * sqrt(3.0));
      glScaled(boundsScale, boundsScale, boundsScale);
    }
      
    glTranslated(
      -(bounds[0].x + bounds[1].x) / 2,
      -(bounds[0].y + bounds[1].y) / 2,
      -(bounds[0].z + bounds[1].z) / 2
      );
      
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    qglColor(Qt::white);

    glLineWidth(1);

    foreach (IMesh *pMesh, mMeshContainer)
    {
      draw_gl_primitives(pMesh, IMesh::element_point, GL_POINTS);
      draw_gl_primitives(pMesh, IMesh::element_line, GL_LINES);
      draw_gl_primitives(pMesh, IMesh::element_quad, GL_QUADS);
      draw_gl_primitives(pMesh, IMesh::element_triangle, GL_TRIANGLES);
    }

    glGetDoublev(GL_MODELVIEW_MATRIX,  m_MatrixModel);
    glGetDoublev(GL_PROJECTION_MATRIX, m_MatrixProjection);
    glGetIntegerv(GL_VIEWPORT, m_Viewport);


    { // Palette
      glDisable(GL_DEPTH_TEST);
      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

      glMatrixMode(GL_MODELVIEW);
      glPushMatrix();
      glLoadIdentity();

      glMatrixMode(GL_PROJECTION);
      glPushMatrix();
      glLoadIdentity();
      const float Width   = 20.0;
      const float Spacing = 30.0;
      glOrtho(0.0, viewport[2], 0.0, viewport[3], -1000, 1000);
      glTranslatef(Spacing, Spacing, 0.0);
      glScalef(1.0, viewport[3] - Spacing * 2, 1.0);

      glBegin(GL_QUAD_STRIP);
      foreach(const QGradientStop & Stop, m_Stops)
      {
        qglColor(Stop.second);
        glVertex2f(0.0,  Stop.first);
        glVertex2f(Width, Stop.first);
      }
      glEnd();

      qglColor(Qt::black);
      foreach(const QGradientStop & Stop, m_Stops)
      {
        const float Value = m_StopMinValue * (1.0 - Stop.first) + m_StopMaxValue * Stop.first;
        renderText(25.0, Stop.first, 0.0, QString("%1").arg(Value));
      }

      glLoadIdentity();
      glOrtho(0.0, viewport[2], 0.0, viewport[3], -1000, 1000);
      renderText(10.0, 10.0, 0.0, m_Caption);

      glPopMatrix();

      glMatrixMode(GL_MODELVIEW);
      glPopMatrix();
    }
  }

  void Mesh::resizeGL(int width, int height)
  {
    glViewport(0, 0, width, height);
    updateGL();
  }

  void Mesh::mouseMoveEvent(QMouseEvent *p_event)
  {
    if (p_manipulator_)
      p_manipulator_->manipulate(p_event->pos());
  }

  void Mesh::mousePressEvent(QMouseEvent *p_event)
  {
    switch (p_event->button())
    {
    case Qt::RightButton:
      { // rotate
        const qreal sensitivity = 0.5;
        p_manipulator_.reset(Manipulator::make_linear(p_event->pos(), sensitivity, 0.0, sensitivity, 0.0));
        connect(p_manipulator_.get(), SIGNAL(manipulate_signal(qreal, qreal)), this, SLOT(set_rotation(qreal, qreal)));
      }
      break;
    case Qt::LeftButton:
      { // pan
        const double k = double(width()) / height();
        p_manipulator_.reset(Manipulator::make_linear(p_event->pos(), zoom_ / width() * k, pan_.x(), -zoom_ / height(), pan_.y()));
        connect(p_manipulator_.get(), SIGNAL(manipulate_signal(qreal, qreal)), this, SLOT(set_pan(qreal, qreal)));
        PickElement(p_event->pos());
        updateGL();
      }
      break;
    case Qt::MidButton:
      { // scale
        const qreal sensitivity = 1.01;
        p_manipulator_.reset(Manipulator::make_exponential(p_event->pos(), 0.0, 0.0, zoom_, sensitivity));
        connect(p_manipulator_.get(), SIGNAL(manipulate_y_signal(qreal)), this, SLOT(set_zoom(qreal)));
      }
      break;
    default:
      return;
    }
    setMouseTracking(true);
  }

  void Mesh::mouseReleaseEvent(QMouseEvent *p_event)
  {
    setMouseTracking(false);

    glLoadMatrixd(m_MatrixRotationManipulator);
    glMultMatrixd(m_MatrixRotation);
    glGetDoublev(GL_MODELVIEW_MATRIX,  m_MatrixRotation);
    glLoadIdentity();
    glGetDoublev(GL_MODELVIEW_MATRIX,  m_MatrixRotationManipulator);
  }

  void Mesh::mouseDoubleClickEvent(QMouseEvent *p_event)
  {
    emit OnElementPickedDouble(IMesh::element_triangle, selectedItemIndex);
  }

  void Mesh::wheelEvent(QWheelEvent *p_event)
  {
    const qreal sensitivity = 1.001;
    scoped_ptr<Manipulator> p_manipulator(Manipulator::make_exponential(QPoint(0, 0), 0.0, 0.0, zoom_, sensitivity));
    connect(p_manipulator.get(), SIGNAL(manipulate_y_signal(qreal)), this, SLOT(set_zoom(qreal)));
    p_manipulator->manipulate(QPoint(0, -p_event->delta()));
  }

  void Mesh::draw_gl_primitives(IMesh *pMesh, IMesh::element_type type, GLenum gl_mode) const
  {
    const IMesh::t_container_vertex  &vertices       = pMesh->get_vertices();
    const IMesh::t_container_color   &vertex_colors  = pMesh->get_vertices_colors();
    const IMesh::t_container_color   &element_colors = pMesh->get_elements_colors(type);
    const IMesh::t_container_element &elements       = pMesh->get_elements(type);
    
    const size_t node_qty = elements.size();
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_COLOR_MATERIAL);
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    
    if (vertex_colors.size() > 0)
    {
      glBegin(gl_mode);
      for (size_t ind = 0; ind < node_qty; ++ind)
      {
        const size_t index = elements[ind];
        glColor3ubv(vertex_colors[index].c);
        glVertex3dv(vertices[index].v);
      }
      glEnd();
    }
    else
    if (element_colors.size() > 0)
    {
      glBegin(gl_mode);
      for (size_t ind = 0; ind < node_qty; ++ind)
      {
        glColor3ubv(element_colors[ind].c);
        glVertex3dv(vertices[elements[ind]].v);
      }
      glEnd();
    }
    
    glDisable(GL_COLOR_MATERIAL);
    
    const bool filled = vertex_colors.size() > 0 || element_colors.size() > 0;

    glEnable(GL_POLYGON_OFFSET_LINE);
    glPolygonOffset(0.0f, -5.0f);
    
    if (wireframe_ || !filled)
    {
      qglColor(Qt::black);
//      glDisable(GL_DEPTH_TEST);
      glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
      glBegin(gl_mode);
      for (size_t ind = 0; ind < node_qty; ++ind)
      {
        const int index = elements[ind];
        glVertex3dv(vertices[index].v);
      }
      glEnd();
//      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }

    if (type == IMesh::element_triangle &&
        selectedItemIndex >= 0)
    {
      glLineWidth(4);
      qglColor(Qt::gray);
      glDisable(GL_DEPTH_TEST);
      glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
      glBegin(gl_mode);
      glVertex3dv(vertices[elements[selectedItemIndex * 3 + 0]].v);
      glVertex3dv(vertices[elements[selectedItemIndex * 3 + 1]].v);
      glVertex3dv(vertices[elements[selectedItemIndex * 3 + 2]].v);
      glEnd();
      glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
      glLineWidth(1);
    }

    glDisable(GL_POLYGON_OFFSET_LINE);
  }
  
  void Mesh::PickElement(const QPoint &pt)
  {
    selectedItemIndex = -1;


    foreach (IMesh *pMesh, mMeshContainer)
    {
      const IMesh::t_container_vertex  & vertices  = pMesh->get_vertices();
      const IMesh::t_container_element & triangles = pMesh->get_elements(IMesh::element_triangle);
      int triangleQty = triangles.size() / 3;
      
      float MinZ = FLT_MAX;
      
      for (int i = 0; i < triangleQty; i++)
      {
        IMesh::vector p1 = vertices[triangles[i * 3 + 0]];
        IMesh::vector p2 = vertices[triangles[i * 3 + 1]];
        IMesh::vector p3 = vertices[triangles[i * 3 + 2]];
        
        gluProject(p1.x, p1.y, p1.z, m_MatrixModel, m_MatrixProjection, m_Viewport, &p1.x, &p1.y, &p1.z);
        gluProject(p2.x, p2.y, p2.z, m_MatrixModel, m_MatrixProjection, m_Viewport, &p2.x, &p2.y, &p2.z);
        gluProject(p3.x, p3.y, p3.z, m_MatrixModel, m_MatrixProjection, m_Viewport, &p3.x, &p3.y, &p3.z);
        
        const qreal MeanZ = (p1.z + p2.z + p3.z) / 3;
        
        if (PtInTriangle(QPointF(pt.x(), m_Viewport[3] - 1 - pt.y()), QPointF(p1.x, p1.y), QPointF(p2.x, p2.y), QPointF(p3.x, p3.y)) &&
            MeanZ < MinZ)
        {
          MinZ = MeanZ;
          selectedItemIndex = i;
        }
      }
    }
    
    emit OnElementPicked(IMesh::element_triangle, selectedItemIndex);
  }
  
  bool Mesh::PtInTriangle(
      const QPointF &pt,
      const QPointF &t1,
      const QPointF &t2,
      const QPointF &t3
    )
  {
    const QPointF d1 = t1 - pt;
    const QPointF d2 = t2 - pt;
    const QPointF d3 = t3 - pt;
    
    const bool case1 = d1.x() * d2.y() - d2.x() * d1.y() < 0.0 &&
                       d2.x() * d3.y() - d3.x() * d2.y() < 0.0 &&
                       d3.x() * d1.y() - d1.x() * d3.y() < 0.0;

    const bool case2 = d1.x() * d2.y() - d2.x() * d1.y() > 0.0 &&
                       d2.x() * d3.y() - d3.x() * d2.y() > 0.0 &&
                       d3.x() * d1.y() - d1.x() * d3.y() > 0.0;
                       
    return case1 || case2;
  }
} // namespace widgets {
